04 - ROS nodes subscribers and publishers
Robotics I
Poznan University of Technology, Institute of Robotics and Machine Intelligence
Laboratory 4: ROS - nodes subscribers and publishers
Goals
- Recap ROS 2 concepts
- Understand a topic-based communication
- Create a publisher node and learn how to send messages to a topic
- Create a simple topics subscriber node to listen to a topic and process incoming messages
Resources
- Creating a ROS 2 package
- Writing a simple publisher and subscriber (Python)
- Writing a simple service and client (C++)
Introduction
We have already learned that most operations in ROS 2 are performed by the nodes, which communicate through topics. In previous lessons, we used some existing nodes and combined them to get the desired results. In practice, we usually need to create our own nodes: either to perform simple data conversions or to perform more sophisticated operations. Therefore, the goal of this lab is to learn how to create nodes and how to receive and send information on selected topics. Today we will do some simple tutorials, while in the next labs we will create a more sophisticated node.
Note: Everything we do today should be done inside the container!
Tasks
Note: Everything we do today should be done inside
the container! To attach another terminal to a running container use the
command docker exec -it <CONTAINER NAME> bash
.
- Run docker container based on osrf/ros:jazzy-desktop-full
docker image with GUI support. Use
docker_run.sh
script provided below.
docker_run.sh
IMAGE_NAME="" # <DOCKER IMAGE REPOSITORY>:<DOCKER IMAGE TAG>
CONTAINER_NAME="" # student ID number
xhost +local:root
XAUTH=/tmp/.docker.xauth
if [ ! -f $XAUTH ]
then
xauth_list=$(xauth nlist :0 | sed -e 's/^..../ffff/')
if [ ! -z "$xauth_list" ]
then
echo $xauth_list | xauth -f $XAUTH nmerge -
else
touch $XAUTH
fi
chmod a+r $XAUTH
fi
docker stop $CONTAINER_NAME || true && docker rm $CONTAINER_NAME || true
docker run -it \
--env="ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST" \
--env="DISPLAY=$DISPLAY" \
--env="QT_X11_NO_MITSHM=1" \
--volume="/tmp/.X11-unix:/tmp/.X11-unix:rw" \
--env="XAUTHORITY=$XAUTH" \
--volume="$XAUTH:$XAUTH" \
--privileged \
--network=host \
--name="$CONTAINER_NAME" \
$IMAGE_NAME \
/bin/bash
- Attach to the running container using VS Code. Follow the steps provided in Laboratory 1 or using Docker extension for VS Code.
Creating nodes
ROS 2 environment
Before you start working with ROS 2, remember to initialize environment variables in each newly opened terminal.
- Before the first compilation initialize global ROS variables:
source /opt/ros/$ROS_DISTRO/setup.bash
- After the first compilation (once build, install, log directories are created) you can source variables from the local project workspace:
source install/setup.bash
Create a package
To create a template package navigate to the ROS 2 workspace, i.e.,
~/ros2_ws/src
directory. Then you can simply call the
following functions to create package in C++ and Python:
- Python
ros2 pkg create --build-type ament_python <PACKAGE NAME>
- C++
ros2 pkg create --build-type ament_cmake <PACKAGE NAME>
Once the project is created it is worth customizing package.xml and setup.py files with license, package description, maintainer data.
Creating a node
Nodes can be created along with a package using
ros2 pkg create
with
--node-name <NODE NAME>
option, but also manually.
The second option is particularly useful if you plan to add additional
nodes an existing package. To do this in Python-based project, follow
these steps:
- Navigate to the
~/ros2_ws/src/<PACKAGE NAME>/<PACKAGE NAME>
directory. - Create a script with the
touch <NODE NAME>.py
command. - Fill in the script.
- In the setup.py file, make the following modification:
={
entry_points'console_scripts': [
'<NODE NAME> = <PACKAGE NAME>.<NODE NAME>:main',
], },
- Build the environment with the
colcon build
command from the~/ros2_ws
directory. - Start the node with the command:
ros2 run <PACKAGE NAME> <NODE NAME>
In the above process, it is important to place the script correctly
(points 1, 2) and add the executable file in entry_points
(point 4).
Note: It is also important to build an environment to observe changes in the program’s performance (point 5) - regardless of whether you are programming in Python or a compiled language (such as C++).
The rclpy library (ROS Client Library for the Python) provides the canonical Python API for interacting with ROS 2. Documentation is available here.
Managing package dependencies
In ROS 2, package runtime dependencies are defined within
<depend> ... </depend>
tags in the
package.xml file. These dependencies are needed when building
and running the package. The <test_depend>
tag is
used to specify dependencies that are only needed for testing purposes.
You can define these dependencies during the build process or add them
later.
The following is an example of a package.xml file that
depends on cv_bridge
, python3-opencv
,
sensor_msgs
, and geometry_msgs
.
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
package format="3">
<name>simple_pkg</name>
<version>0.0.0</version>
<description>TODO: Package description</description>
<maintainer email="root@todo.todo">root</maintainer>
<license>TODO: License declaration</license>
<
depend>cv_bridge</depend>
<depend>python3-opencv</depend>
<depend>sensor_msgs</depend>
<depend>geometry_msgs</depend>
<
test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<
export>
<build_type>ament_python</build_type>
<export>
</package> </
To install workspace or specific package dependencies (packages, libraries) you can use rosdep. This tool performs this process automatically by reading the dependencies defined in the package.xml files.
Before installing / updating dependencies it is worth to update the local indexes from the rosdistro package database.
rosdep update
Then, from the ROS 2 workspace, i.e. ~/ros2_ws
, run the
following command, which will install the dependencies for all packages
in the src
directory.
rosdep install --from-paths src -y --ignore-src --rosdistro <ROS DISTRO>
In our case, the ROS 2 distribution is jazzy
.
Tasks
Note: Every time you open a terminal window and want
to work with ROS 2 you need to source environmental variables using
source /opt/ros/$ROS_DISTRO/setup.bash
or
source install/setup.bash
if you work in a specific ROS 2
workspace (e.g. ~/ros2_ws
)!
- Create ROS 2 workspace, i.e.,
~/ros2_ws
, withsrc
directory. Insrc
folder create a Python package namedsimple_pkg
with a node namedsimple_node
. - Fill the
simple_node.py
file with the following source code:
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
def main(args=None):
=args)
rclpy.init(args= Node('simple_node')
node 'Simple node started!')
node.get_logger().info(
rclpy.spin(node)
rclpy.shutdown()
if __name__ == '__main__':
main()
- Build package, source the setup file, and run the created node.
- Perform the following test. Edit the above source code of
simple_node.py
(Task 4) by commenting out the line containingrclpy.spin(node)
. Observe the effect and consider what the purpose of this line of code is. Provide the answer in the form of a text file to the eKursy platform.
Publisher
ROS 2 documentation on node creation indicates the object-oriented method as the default for creating nodes, which is useful for creating scalable and modular code.
The following source code illustrates the template publisher node,
which publishes std_msgs/msg/String
message on topic_name
topic. Note that this is a simple
example but in practice a single node can have numerous publishers and
subscribers.
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalPublisher(Node):
def __init__(self):
super().__init__('minimal_publisher')
self.publisher_ = self.create_publisher(String, 'topic_name', 10)
= 0.5 # seconds
timer_period self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
def timer_callback(self):
= String()
msg = f'Hello World: {self.i}'
msg.data self.publisher_.publish(msg)
self.get_logger().info(f'Publishing: "{msg.data}"')
self.i += 1
def main(args=None):
=args)
rclpy.init(args
= MinimalPublisher()
minimal_publisher
rclpy.spin(minimal_publisher)
rclpy.shutdown()
if __name__ == '__main__':
main()
Starting with the definition of the class constructor,
super().__init__
internally calls the Node
class constructor and gives a name to the node, in this case
minimal_publisher. The create_publisher
method
creates an object that publishes messages of type std_msgs/msg/String
(imported from the std_msgs.msg library) on the topic_name
topic. In addition, the value 10
given as an argument to
this function indicates the buffer size. It is useful in case when the
subscriber cannot keep up with processing messages, resulting in
discarding incoming messages when the buffer size is exceeded.
In the above code a timer is created with a callback that executes
every 0.5 seconds. The timer_callback
function creates a
message containing the state of the counter (self.i
),
displays it in the console using the get_logger().info
method and publishes it on the topic.
Further explanation with code examination and entry point definition is described in the article Write the publisher node.
Tasks
- Following the steps from Creating a
node section create
minimal_publisher
node and add to the minimal_publisher.py file the above source code. Then, updateentry_points
section in setup.py and build the environment with thecolcon build
command. Thereafter source the ROS 2 workspace and runminimal_publisher
node usingros2 run
command. From another terminal check the published messages using theros2 topic echo
command.
Subscriber
In ROS 2 subscriber is a node that listens to messages from a
specific topic published by other nodes in a ROS 2 system. It works by
subscribing to the topic, receiving the published messages, and
processing them based on the it’s callback function. Note that a node
can simultaneously publishing and subscribing topics. The following
source code illustrates the template subscriber node, which subscribes
topic_name
topic with message of type std_msgs/msg/String.
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('minimal_subscriber')
self.subscription = self.create_subscription(
String,'topic_name',
self.listener_callback,
10)
self.subscription # prevent unused variable warning
def listener_callback(self, msg):
self.get_logger().info(f'I heard: "{msg.data}"')
def main(args=None):
=args)
rclpy.init(args
= MinimalSubscriber()
minimal_subscriber
rclpy.spin(minimal_subscriber)
rclpy.shutdown()
if __name__ == '__main__':
main()
The source code presented for the subscriber node is almost identical
to the code used to write the publisher. The constructor,
super().__init__
, internally calls the Node
class constructor and gives the node a name, in this case
minimal_subscriber.
To successfully subscribe to a topic and communicate, it is necessary
that the topic name and message type used by the publisher and
subscriber are the same. Unlike minimal_publisher, the
subscriber’s constructor and callback don’t include a timer definition,
because it doesn’t need one - its callback,
listener_callback
, is called as soon as node receives a
message. In this case, the callback definition simply prints an info
message to the console, along with the data it received.
Further explanation with code examination and entry point definition is described in the article Write the subscriber node
Tasks
- Following the steps from Creating a
node section create
minimal_subscriber
node and add to the minimal_subscriber.py file the above source code. Then, updateentry_points
section in setup.py and build the environment with thecolcon build
command. Thereafter source the ROS 2 workspace and runminimal_subscriber
node usingros2 run
command. In one terminal runminimal_publisher
node from previous task, while from the other terminal runminimal_subscriber
node and verify that messages are received.
Nodes in different programming languages
ROS2 Client Libraries is an API that, allows users to implement ROS code. Using Client Libraries, code developers gain access to concepts such as nodes, topics, services, etc. The Client Libraries are available in a variety of programming languages, allowing users to write ROS code in the language that best suits their application. For example, you may want to write visualization tools in Python because it speeds up prototyping, while efficient nodes can be implemented in C++.
Sample template for creating an object node for C++ language:
#include "rclcpp/rclcpp.hpp"
class MyCustomNode : public rclcpp::Node
{
public:
() : Node("node_name")
MyCustomNode{
}
private:
};
int main(int argc, char **argv)
{
::init(argc, argv);
rclcppauto node = std::make_shared<MyCustomNode>();
::spin(node);
rclcpp::shutdown();
rclcppreturn 0;
}
Nodes written in different programming languages can still
communicate because they are built on a common rcl
(ROS2
Client Libraries) interface.
Source: https://ai-sinq.tistory.com
ROS functionalities for C++(rclcpp
) and Python
(rclpy
) are managed and supported by ROS authors and teams.
On the other hand, there are also other languages for which
functionalities are created by the ROS community (e.g. Rust, Node.js, C,
Android). More information here.
Tasks
- Based on the TurtleSim
simulator from previous lab, create a simple node that listens to the
topic
/turtle1/goal
of type geometry_msgs/Point, which defines the point your turtle should reach. The node should continuously publish a sequence of the turtle’s steering velocities to/turtle1/cmd_vel
that will move the turtle until the turtle is close to the goal point. The turtle destination can be specified from the below command. Upload the compressed package (node) with your solution to eKursy.
ros2 topic pub /turtle1/goal geometry_msgs/Point "{x: 6.0, y: 7.0, z: 0.0}" --once
Tip 1: The turtle is NOT spawned at position (0, 0)
based on messages available at /turtle1/pose
. X is a
horizontal axis, while Y is vertical.
Tip 2: The goal and pose are specified in the global
coordinate system while cmd_vel
is provided in the local
coordinate system of the turtle.